跳转至

Postgres 语言服务器:初始版本正式发布

文章背景与核心概要

经过长达两年的研发历程(涵盖深入调研、架构转变以及 Rust 语言的学习),Postgres 语言服务器(Postgres Language Server) 的首个版本现已正式发布。这是一个实现语言服务器协议(LSP)的工具集,旨在为开发者提供可靠的 SQL 编写体验。

本文详细介绍了该语言服务器的核心架构、技术挑战以及文档生命周期管理。通过结合 libpg_query 进行精确解析、利用 tree-sitter 处理不完整语句,并借助内存中的 SchemaCache 提供上下文感知的自动补全和类型检查,该项目成功克服了 SQL 语法复杂且缺乏直接文件依赖关系的难题。此外,文章还展望了其未来在 WebAssembly(Wasm)和 PL/pgSQL 支持方面的演进方向。


Postgres Language Server: Initial Release

After a two-year development journey involving research, architectural shifts, and learning Rust, the initial release of the Postgres Language Server is officially here. It is a Language Server Protocol (LSP) implementation and a collection of developer tools designed for reliable SQL authoring.

此版本提供以下核心功能: * 自动补全 * 语法错误高亮 * 类型检查(通过 EXPLAIN 错误洞察实现) * 代码检查(Linter),灵感来源于 Squawk

This release provides: * Autocompletion * Syntax Error Highlighting * Type-checking (via EXPLAIN error insights) * Lining, inspired by Squawk


可用性与安装

您可以通过各种编辑器扩展和命令行(CLI)包来使用该语言服务器:

有关更多详细信息,请查阅官方文档GitHub 仓库

Availability and Installation

You can access the Language Server through various editor extensions and CLI packages:

For further details, check out the documentation or the GitHub repository.


语言服务器的架构

选择正确的数据模型在很大程度上取决于目标语言。例如,C++ 由于其头文件和“先声明后使用”的规则,会编译一次头文件并将其缓存。TypeScript 则会编译源文件(如 foo.ts)来解析依赖文件的类型。

然而,对于 SQL 和 Postgres 而言,规则则有所不同: * 数据库架构(Schema)是任何类型信息的唯一真实数据源(Single Source of Truth)。 * 不存在直接的文件到文件(file-to-file)关系。 * 架构管理工具各不相同,这意味着我们无法预设源代码的结构。

因此,Postgres 语言服务器作出了如下假设: 1. 编译的最小单位是单个语句(Single Statement)。 2. 数据库架构是任何类型信息的唯一真实数据源。 3. 所有语句都是独立的:为了理解任何语句,我们只需要解析该特定语句,而不需要其他内容。

The Architecture of a Language Server

Choosing the right data model depends heavily on the target language. For example, C++ compiles headers once and caches them because of its header files and declaration-before-use rules. TypeScript compiles source files (like foo.ts) to resolve types for dependent files.

For SQL and Postgres, however, the rules are different: * The database schema is the single source of truth for any type information. * There is no direct file-to-file relation. * Schema management tools vary widely, meaning we cannot assume how source code is structured.

Therefore, the Postgres Language Server assumes that: 1. The smallest unit of compilation is a single statement. 2. The database schema is the single source of truth for any type information. 3. All statements are independent: to make sense of any statement, we only need to parse that specific statement and nothing else.


技术挑战

该项目最复杂的部分是解析器(Parser)。由于 Postgres 拥有不断演进且复杂的语法,从头开始编写自定义解析器极其困难。

为了绕过这一难题,该项目利用 libpg_query 来可靠地解析 SQL 代码。然而,由于 libpg_query 是为可执行 SQL 设计的,而非针对语言智能(Language Intelligence),开发团队必须围绕文档生命周期构建出务实的解决方案。

Technical Challenges

The most complex part of the project was the parser. Because Postgres has an ever-evolving and complex syntax, writing a custom parser from scratch is exceptionally difficult.

To bypass this, the project utilizes libpg_query to parse SQL code reliably. However, since libpg_query is designed for executable SQL rather than language intelligence, the development team had to build pragmatic solutions around the document lifecycle.


文档生命周期

1. 拆分源码

当打开新文档时,一个自定义的语句拆分器会将文件(其中可能包含无效或不完整的语句)切分为独立的语句,以便 libpg_query 能够独立处理它们。

普拉特解析法(Pratt Parsing) 启发,该拆分器使用了智能启发式算法(例如,深知除非作为子语句,否则 SELECT 后面不能紧跟另一个 SELECT)。如果解析失败,它会退回到按分号或双换行符进行拆分。

2. 识别语句

文档中的每个语句都会被分配一个唯一标识符,并通过 Statement 结构体进行追踪:

/// 全局唯一的语句
#[derive(Hash)]
pub(crate) struct Statement {
  /// 文档路径
  pub(crate) path: PgTPath,
  /// 文档内的唯一 ID
  pub(crate) id: StatementId,
}

3. 解析语句

服务器在这些语句上同时运行 tree-sitterlibpg_query: * libpg_query 提供精确的解析、语法错误检测以及用于诊断的 AST(抽象语法树)结构。 * tree-sitter 即使面对格式错误或不完整的语句也能生成语法树,这对于实时编辑和自动补全至关重要。

Document Lifecycle

1. Splitting the Source

When opening a new document, a custom statement splitter cuts the file (which may contain invalid or incomplete statements) into individual statements so libpg_query can process them independently.

Inspired by Pratt Parsing, the splitter uses smart heuristics (e.g., knowing that a SELECT cannot be followed by another SELECT unless it is a sub-statement). If parsing fails, it falls back to splitting at semicolons or double newlines.

2. Identifying Statements

Each statement is assigned a unique identifier within the document and tracked via a Statement struct:

/// Globally unique statement
#[derive(Hash)]
pub(crate) struct Statement {
  /// Path of the document
  pub(crate) path: PgTPath,
  /// Unique id within the document
  pub(crate) id: StatementId,
}

3. Parsing the Statements

The server runs both tree-sitter and libpg_query on the statements: * libpg_query provides precise parsing, syntax error detection, and AST structures for diagnostics. * tree-sitter generates syntax trees even for malformed or incomplete statements, making it invaluable for live editing and autocompletion.


加载架构信息

为了提供上下文分析,服务器会采用类似于 PostgREST 的方式,延迟填充(lazily populates)一个内存中的架构缓存:

pub struct SchemaCache {
  pub schemas: Vec<Schema>,
  pub tables: Vec<Table>,
  pub functions: Vec<Function>,
  pub types: Vec<PostgresType>,
  pub versions: Vec<Version>,
  pub columns: Vec<Column>,
}

Loading Schema Information

To provide contextual analysis, the server lazily populates an in-memory schema cache similar to PostgREST:

pub struct SchemaCache {
  pub schemas: Vec<Schema>,
  pub tables: Vec<Table>,
  pub functions: Vec<Function>,
  pub types: Vec<PostgresType>,
  pub versions: Vec<Version>,
  pub columns: Vec<Column>,
}

提供诊断与处理变更

  • 诊断(Diagnostics): 服务器将 libpg_query 的 AST 传递给类型检查器(该检查器使用 Postgres 的 PREPARE 语句来捕获数据库级别的类型错误),并结合受 Squawk 启发的 linter 进行代码检查。
  • 响应式编辑(Responsive Edits): 由于 SQL 语句是独立的,文本编辑只需要将修改后的语句作废,并调整后续语句的范围——从而避免了对整个文档进行重新解析。

Providing Diagnostics & Processing Changes

  • Diagnostics: The server passes the libpg_query AST to a type checker (which uses Postgres PREPARE statements to catch database-level type errors) and a linter inspired by Squawk.
  • Responsive Edits: Because SQL statements are independent, text edits only require invalidating the modified statement and shifting the ranges of subsequent statements—avoiding full-document re-parsing.

响应式自动补全

自动补全完全依赖于 tree-sitter(用于处理不完整的语句)以及内存中的 SchemaCache(避免了昂贵的数据库查询)。评分算法会根据上下文评估建议: * 如果用户位于 SELECT 子句内部,表建议的权重会降低。 * 如果存在 FROM 子句,该表的列建议权重会提高。

评分逻辑示例:

fn check_matches_schema(
  /// `self` 是当前被考察的建议项。
    &mut self,
  /// `ctx` 包含有关更改后的 CST 节点以及包含该语句的信息。
    ctx: &CompletionContext
) {
  let schema_name = match ctx.schema_name.as_ref() {
    None => return,
    Some(n) => n,
  };

  let data_schema = self.get_schema_name();

  if schema_name == data_schema {
    self.score += 25;
  } else {
    self.score -= 10;
  }
}

Responsive Autocompletion

Autocompletion relies exclusively on tree-sitter (to handle incomplete statements) and the in-memory SchemaCache (avoiding expensive database queries). A scoring algorithm evaluates suggestions based on context: * If the user is inside a SELECT clause, table suggestions are down-ranked. * If a FROM clause is present, column suggestions for that table are up-ranked.

Example scoring logic:

fn check_matches_schema(
  /// `self` is the currently investigated suggestion item.
  &mut self,
  /// `ctx` contains the information about the changed CST node and the
  /// containing statement.
  ctx: &CompletionContext
) {
  let schema_name = match ctx.schema_name.as_ref() {
    None => return,
    Some(n) => n,
  };

  let data_schema = self.get_schema_name();

  if schema_name == data_schema {
    self.score += 25;
  } else {
    self.score -= 10;
  }
}


不仅仅是一个语言服务器

尽管语言服务器是核心重点,但底层的 API 工作空间采用了受 Biome 启发 的客户端-服务器架构。这使得工具链能够通过以下方式运行: * 通过 CLI(postgrestools check test.sql) * 在 HTTP API 背后运行 * 最终通过 Wasm 直接在浏览器中运行

Not Just a Language Server

While the Language Server is the primary focus, the underlying workspace API is built with a client-server architecture inspired by Biome. This allows the toolchain to run: * Via the CLI (postgrestools check test.sql) * Behind an HTTP API * Eventually via Wasm directly in the browser


未来展望

接下来的里程碑包括: * 提高稳定性并修复边缘用例(edge cases)。 * 发布安装指南和更多的编辑器扩展。 * 增加对 PL/pgSQL 的支持、针对 pglite 的 Wasm 构建版本,以及对 SQL 函数体的解析。

热烈欢迎社区的贡献、错误报告和反馈!

What’s Next

Upcoming milestones include: * Improving stability and fixing edge cases. * Publishing installation guides and additional editor extensions. * Adding PL/pgSQL support, a Wasm build for pglite, and parsing SQL function bodies.

Contributions, bug reports, and feedback from the community are warmly welcomed!